﻿using System;
using System.Xml;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Security.Cryptography.X509Certificates;


namespace InfoJetSoft.Service.Util
{
    public class KeyFromCert
    {

        /// This function creates and returns an RSACryptoServiceProvider 
        /// object (containing only the public key) based on the supplied 
        /// XmlDSig X509Certificate element.
        public static RSACryptoServiceProvider GetPublicKeyFromCertElement(
            XmlElement certElement)
        {
            // Get an X509Certificate from the XML element.
            X509Certificate cert = CreateX509CertificateFromXMLElement(
                certElement);

            // Return the RSA public key from the certificate.
            return GetPublicKeyFromX509Certificate(cert);
        }

        /// 
        /// This method takes an XML element which contains a base64-encoded 
        /// X509Certificate in ASN.1 format and loads it into a 
        /// System.Security.Cryptography.X509Certificates.X509Certificate 
        /// object.
        /// 
        /// The XML element containing
        /// the encoded X509Certificate
        /// A new
        /// System.Security.Cryptography.X509Certificates.X509Certificate 
        /// object
        private static X509Certificate CreateX509CertificateFromXMLElement(
            XmlElement theCertElement)
        {
            // Make sure the name of the element is "X509Certificate",
            // to confirm the user is making a good-faith effort to provide 
            // the proper data.
            if (theCertElement.LocalName != "X509Certificate")
            {
                throw new System.Exception("Bad element name!");
            }

            // The text of the element should be a Base64 representation 
            // of the certificate, so load it as a string.
            String base64CertificateData = theCertElement.InnerText;

            // Remove any whitespace that may be cluttering up the data.
            base64CertificateData = base64CertificateData.Replace("\n", "");
            base64CertificateData = base64CertificateData.Replace("\r", "");
            base64CertificateData = base64CertificateData.Replace("\f", "");
            base64CertificateData = base64CertificateData.Replace("\t", "");
            base64CertificateData = base64CertificateData.Replace(" ", "");

            // Convert the data to a byte array.
            byte[] certificateData = System.Convert.FromBase64String(
                base64CertificateData);

            // Create a new X509Certificate from the data.
            System.Security.Cryptography.X509Certificates.X509Certificate cert =
                new System.Security.Cryptography.X509Certificates.X509Certificate(
                certificateData);

            //Return the result
            return cert;
        }

        /// 
        /// This function creates and returns an RSACryptoServiceProvider 
        /// object (containing only the public key) based on the
        /// supplied X509Certificate object.
        /// 
        /// the X509Certificate object from which 
        /// to extract a public key.
        /// A System.Security.Cryptography.RSACryptoServiceProvider
        /// object
        private static RSACryptoServiceProvider
            GetPublicKeyFromX509Certificate(X509Certificate x509)
        {
            // This code has been adapted from the KnowledgeBase article
            // 320602 HOW TO: Sign and Verify SignedXml Objects Using Certificates
            // http://support.microsoft.com/?id=320602

            RSACryptoServiceProvider rsacsp = null;
            uint hProv = 0;
            IntPtr pPublicKeyBlob = IntPtr.Zero;

            // Get a pointer to a CERT_CONTEXT from the raw certificate data.
            IntPtr pCertContext = IntPtr.Zero;
            pCertContext = (IntPtr)CertCreateCertificateContext(
                X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                x509.GetRawCertData(),
                x509.GetRawCertData().Length);

            if (pCertContext == IntPtr.Zero)
            {
                Console.WriteLine("CertCreateCertificateContext failed: "
                    + Marshal.GetLastWin32Error().ToString());
                goto Cleanup;
            }

            if (!CryptAcquireContext(ref hProv, null, null, PROV_RSA_FULL, 0))
            {
                if (!CryptAcquireContext(ref hProv, null, null, PROV_RSA_FULL,
                    CRYPT_NEWKEYSET))
                {
                    Console.WriteLine("CryptAcquireContext failed: "
                        + Marshal.GetLastWin32Error().ToString());
                    goto Cleanup;
                }
            }

            // Get a pointer to the CERT_INFO structure.
            // It is the 4th DWORD of the CERT_CONTEXT structure.
            //
            //    typedef struct _CERT_CONTEXT {
            //        DWORD         dwCertEncodingType;
            //        BYTE*         pbCertEncoded;
            //        DWORD         cbCertEncoded;
            //        PCERT_INFO    pCertInfo;
            //        HCERTSTORE    hCertStore;
            //    } CERT_CONTEXT,  *PCERT_CONTEXT;
            //    typedef const CERT_CONTEXT *PCCERT_CONTEXT;
            //
            IntPtr pCertInfo = (IntPtr)Marshal.ReadInt32(pCertContext, 12);

            // Get a pointer to the CERT_PUBLIC_KEY_INFO structure.
            // This structure is located starting at the 57th byte
            // of the CERT_INFO structure.
            // 
            //    typedef struct _CERT_INFO {
            //        DWORD                       dwVersion;
            //        CRYPT_INTEGER_BLOB          SerialNumber;
            //        CRYPT_ALGORITHM_IDENTIFIER  SignatureAlgorithm;
            //        CERT_NAME_BLOB              Issuer;
            //        FILETIME                    NotBefore;
            //        FILETIME                    NotAfter;
            //        CERT_NAME_BLOB              Subject;
            //        CERT_PUBLIC_KEY_INFO        SubjectPublicKeyInfo;
            //        CRYPT_BIT_BLOB              IssuerUniqueId;
            //        CRYPT_BIT_BLOB              SubjectUniqueId;
            //        DWORD                       cExtension;
            //        PCERT_EXTENSION             rgExtension;
            //    } CERT_INFO, *PCERT_INFO;
            // 
            IntPtr pSubjectPublicKeyInfo = (IntPtr)(pCertInfo.ToInt32() + 56);

            // Import the public key information from the certificate context
            // into a key container by passing the pointer to the 
            // SubjectPublicKeyInfo member of the CERT_INFO structure 
            // into CryptImportPublicKeyInfoEx.
            // 
            uint hKey = 0;
            if (!CryptImportPublicKeyInfoEx(hProv, X509_ASN_ENCODING |
                PKCS_7_ASN_ENCODING, pSubjectPublicKeyInfo, 0, 0, 0, ref hKey))
            {
                Console.WriteLine("CryptImportPublicKeyInfoEx failed: "
                    + Marshal.GetLastWin32Error().ToString());
                goto Cleanup;
            }

            // Now that the key is imported into a key container use
            // CryptExportKey to export the public key to the PUBLICKEYBLOB
            // format.
            // First get the size of the buffer needed to hold the 
            // PUBLICKEYBLOB structure.
            // 
            uint dwDataLen = 0;
            if (!CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0, 0, ref dwDataLen))
            {
                Console.WriteLine("CryptExportKey failed: "
                    + Marshal.GetLastWin32Error().ToString());
                goto Cleanup;
            }

            // Then export the public key into the PUBLICKEYBLOB format.
            pPublicKeyBlob = Marshal.AllocHGlobal((int)dwDataLen);
            if (!CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0,
                (uint)pPublicKeyBlob.ToInt32(), ref dwDataLen))
            {
                Console.WriteLine("CryptExportKey failed: "
                    + Marshal.GetLastWin32Error().ToString());
                goto Cleanup;
            }

            // The PUBLICKEYBLOB has the following format:
            //        BLOBHEADER blobheader;
            //        RSAPUBKEY rsapubkey;
            //        BYTE modulus[rsapubkey.bitlen/8];
            // 
            // Which can be expanded to the following:
            // 
            //        typedef struct _PUBLICKEYSTRUC {
            //            BYTE   bType;
            //            BYTE   bVersion;
            //            WORD   reserved;
            //            ALG_ID aiKeyAlg;
            //        } BLOBHEADER, PUBLICKEYSTRUC;
            //        typedef struct _RSAPUBKEY {
            //            DWORD magic;
            //            DWORD bitlen;
            //            DWORD pubexp;
            //        } RSAPUBKEY;
            //        BYTE modulus[rsapubkey.bitlen/8];

            // Get the public exponent.
            // The public exponent is located in bytes 17 through 20 of the 
            // PUBLICKEYBLOB structure.
            byte[] Exponent = new byte[4];
            Marshal.Copy((IntPtr)(pPublicKeyBlob.ToInt32() + 16), Exponent, 0, 4);
            Array.Reverse(Exponent); // Reverse the byte order.

            // Get the length of the modulus.
            // To do this extract the bit length of the modulus 
            // from the PUBLICKEYBLOB. The bit length of the modulus is at bytes 
            // 13 through 17 of the PUBLICKEYBLOB.
            int BitLength = Marshal.ReadInt32(pPublicKeyBlob, 12);

            // Get the modulus. The modulus starts at the 21st byte of the 
            // PUBLICKEYBLOB structure and is BitLengh/8 bytes in length.
            byte[] Modulus = new byte[BitLength / 8];
            Marshal.Copy((IntPtr)(pPublicKeyBlob.ToInt32() + 20), Modulus, 0,
                BitLength / 8);
            Array.Reverse(Modulus); // Reverse the byte order.

            // Put the modulus and exponent into an RSAParameters object.
            RSAParameters rsaparms = new RSAParameters();
            rsaparms.Exponent = Exponent;
            rsaparms.Modulus = Modulus;

            // Import the modulus and exponent into an RSACryptoServiceProvider
            // object via the RSAParameters object.
            rsacsp = new RSACryptoServiceProvider();
            rsacsp.ImportParameters(rsaparms);

        Cleanup:

            if (pCertContext != IntPtr.Zero)
                CertFreeCertificateContext(pCertContext);

            if (hProv != 0)
                CryptReleaseContext(hProv, 0);

            if (pPublicKeyBlob != IntPtr.Zero)
                Marshal.FreeHGlobal(pPublicKeyBlob);

            return rsacsp;
        }

        private const int X509_ASN_ENCODING = 0x00000001;
        private const int PKCS_7_ASN_ENCODING = 0x00010000;
        private const int PROV_RSA_FULL = 1;
        private const int PUBLICKEYBLOB = 0x6;
        private const int CRYPT_NEWKEYSET = 0x00000008;

        // Import statements for calls to Crypto API and Win32 API methods.
        [DllImport("Crypt32.DLL", EntryPoint = "CertCreateCertificateContext",
            SetLastError = true,
            CharSet = CharSet.Unicode, ExactSpelling = false,
            CallingConvention = CallingConvention.StdCall)]
        private static extern IntPtr CertCreateCertificateContext(
             int dwCertEncodingType,
             byte[] pbCertEncoded,
             int cbCertEncoded);

        [DllImport("Crypt32.DLL", EntryPoint = "CryptAcquireContextU",
            SetLastError = true,
            CharSet = CharSet.Unicode, ExactSpelling = false,
            CallingConvention = CallingConvention.StdCall)]
        private static extern bool CryptAcquireContext(
             ref uint phProv, string szContainer,
             string szProvider,
             int dwProvType,
             int dwFlags);

        [DllImport("Crypt32.DLL", EntryPoint = "CryptImportPublicKeyInfoEx",
            SetLastError = true,
            CharSet = CharSet.Unicode, ExactSpelling = false,
            CallingConvention = CallingConvention.StdCall)]
        private static extern bool CryptImportPublicKeyInfoEx(
            uint hCryptProv,
            uint dwCertEncodingType,
            IntPtr pInfo,
            int aiKeyAlg,
            int dwFlags,
            int pvAuxInfo,
            ref uint phKey);

        [DllImport("Advapi32.DLL", EntryPoint = "CryptExportKey",
            SetLastError = true,
            CharSet = CharSet.Unicode, ExactSpelling = false,
            CallingConvention = CallingConvention.StdCall)]
        private static extern bool CryptExportKey(
            uint hKey,
            uint hExpKey,
            int dwBlobType,
            int dwFlags,
            uint pbData,
            ref uint pdwDataLen);

        [DllImport("Crypt32.DLL", EntryPoint = "CertFreeCertificateContext",
            SetLastError = true,
            CharSet = CharSet.Unicode, ExactSpelling = false,
            CallingConvention = CallingConvention.StdCall)]
        private static extern bool CertFreeCertificateContext(
            IntPtr pCertContext);

        [DllImport("Advapi32.DLL", EntryPoint = "CryptReleaseContext",
            SetLastError = true,
            CharSet = CharSet.Unicode, ExactSpelling = false,
            CallingConvention = CallingConvention.StdCall)]
        private static extern bool CryptReleaseContext(
            uint hProv,
            int dwFlags);
    }
}
